package com.googlecode.micecode;
/*
* Copyright (c) 2011, Anton Babushkin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the nameSuffix of the <organization> nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
 * MiceCode designed as memory and speed effective alternative to JSON, optimized
 * for modern object-oriented languages like Python or Java. It's more powerful
 * than Bencode or BSON, and more effective than JSON. Protocol ideology
 * influenced by JSON, Bencode, FAST. MiceCode have the following data types:
 * Integer, Float, Boolean, None, String, Bytes, List, Map, Decimal. Floats, Integers and
 * Decimals are stored in the smallest possible representation that doesn't worse
 * precision. Protocol supports integers up to 2^64 bytes, floats up to 64 bits and
 * string/bytes length up to 2^64. This realization limitations:
 * floats up to 64 bits only (double).
 *
 * @author Anton Babushkin
 * @version 1.0
 */

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

public final class MiceCode {
    private static final byte BITS_SPECIAL = 0x00;
    private static final byte BITS_INTEGER = 0x10;
    private static final byte BITS_FLOAT = 0x20;
    private static final byte BITS_BYTES = 0x30;
    private static final byte BITS_STRING = 0x40;
    private static final byte BITS_LIST = 0x50;
    private static final byte BITS_MAP = 0x60;
    private static final byte BITS_DECIMAL = 0x70;

    private static final byte BITS_FALSE = 0x00;
    private static final byte BITS_TRUE = 0x01;
    private static final byte BITS_NONE = 0x0f;

    public final static class MiceDecoder {
        private final InputStream stream;

        public MiceDecoder(InputStream stream) {
            this.stream = stream;
        }

        public Object read() throws IOException {
            int h = stream.read();
            int t = h & 0xf0;    // Object type
            if (t == BITS_SPECIAL) {
                int v = h & 0x0f;
                if (v == BITS_FALSE) {
                    return Boolean.FALSE;
                } else if (v == BITS_TRUE) {
                    return Boolean.TRUE;
                } else if (v == BITS_NONE) {
                    return null;
                } else {
                    throw new RuntimeException(String.format("Invalid special bits: %02x", v));
                }
            } else if (t == BITS_INTEGER) {
                return readInteger(h);
            } else if (t == BITS_FLOAT) {
                return readFloat(h);
            } else if (t == BITS_BYTES) {
                return readBytes(h);
            } else if (t == BITS_STRING) {
                return readString(h);
            } else if (t == BITS_LIST) {
                return readList(h);
            } else if (t == BITS_MAP) {
                return readMap(h);
            } else if (t == BITS_DECIMAL) {
                return readDecimal(h);
            }
            throw new RuntimeException(String.format("Invalid byte stream: %02x", t));
        }

        private int readSize(int h) throws IOException {
            int s = h & 0x0f;
            if (s <= 0x08) {
                return s;
            } else {
                if ((s & 0x07) > 4)
                    throw new RuntimeException("Size field bytes number > 4");
                int size = (s & 0x07);
                int res = 0;
                for (int i = 0; i < size; i++)
                    res = res << 8 | stream.read();
                res += 0x09;
                return res;
            }
        }

        private BigInteger readInteger(int h) throws IOException {
            byte[] b = new byte[readSize(h) + 1];
            stream.read(b);
            return new BigInteger(b);
        }

        private Number readFloat(int h) throws IOException {
            int s = h & 0x0f;
            if (s == 0x03) {    // Single precision
                int r = 0;
                for (int i = 0; i < 4; i++)
                    r = r << 8 | stream.read();
                return Float.intBitsToFloat(r);
            } else if (s == 0x07) {    // Double precision
                long r = 0;
                for (int i = 0; i < 8; i++)
                    r = r << 8 | (long) stream.read();
                return Double.longBitsToDouble(r);
            } else {
                throw new RuntimeException(String.format("Invalid float, header: %x", h));
            }
        }

        private byte[] readBytes(int h) throws IOException {
            byte[] rv = new byte[readSize(h)];
            stream.read(rv);
            return rv;
        }

        private String readString(int h) throws IOException {
            byte[] rv = new byte[readSize(h)];
            stream.read(rv);
            return new String(rv, "UTF-8");
        }

        private List<Object> readList(int h) throws IOException {
            int size = readSize(h);
            final List<Object> rv = new ArrayList<Object>(size);
            for (int i = 0; i < size; i++) {
                rv.add(read());
            }
            return rv;
        }

        private Map<String, Object> readMap(int h) throws IOException {
            final Map<String, Object> rv = new HashMap<String, Object>();
            int size = readSize(h);
            for (int i = 0; i < size; i++) {
                String k = (String) read();
                rv.put(k, read());
            }
            return rv;
        }

        private BigDecimal readDecimal(int h) throws IOException {
            BigInteger m = readInteger(h);
            int e = stream.read();
            if ((e & 0x80) != 0) {      // Multi-byte scale
                int sbl = (e & 0x7f) + 1;
                if (sbl > 4)
                    throw new RuntimeException("Decimal scale > 32bit is not supported");
                byte[] sb = new byte[sbl];
                stream.read(sb);
                e = new BigInteger(sb).intValue();
                if (e >= 0)
                    e += 0x40;
                else
                    e -= 0x40;
            } else {                    // Single-byte scale
                if (e >= 0x40)
                    e = -0x80 + e;
            }
            return new BigDecimal(m, -e);
        }
    }

    public final static class MiceEncoder {
        private final OutputStream stream;

        public MiceEncoder(OutputStream stream) {
            this.stream = stream;
        }

        public MiceEncoder write(Object obj) throws IOException {
            if (obj == null) {
                stream.write(BITS_NONE);   // | BITS_SPECIAL
            } else if (obj instanceof Boolean) {
                if ((Boolean) obj) {
                    stream.write(BITS_TRUE);   // | BITS_SPECIAL
                } else {
                    stream.write(BITS_FALSE);   // | BITS_SPECIAL
                }
            } else if ((obj instanceof Short) || (obj instanceof Integer) || (obj instanceof Long) ||
                    (obj instanceof BigInteger)) {
                writeInteger((Number) obj, BITS_INTEGER);
            } else if ((obj instanceof Float) || (obj instanceof Double)) {
                writeFloat((Number) obj);
            } else if (obj instanceof byte[]) {
                writeBytes((byte[]) obj);
            } else if (obj instanceof String) {
                writeString((String) obj);
            } else if (obj instanceof Collection) {
                //noinspection unchecked
                writeList((Collection<Object>) obj);
            } else if (obj instanceof Map) {
                //noinspection unchecked
                writeMap((Map<String, Object>) obj);
            } else if (obj instanceof Object[]) {
                //noinspection unchecked
                writeList((Object[]) obj);
            } else if (obj instanceof int[]) {
                //noinspection unchecked
                writeList((int[]) obj);
            } else if (obj instanceof long[]) {
                //noinspection unchecked
                writeList((long[]) obj);
            } else if (obj instanceof float[]) {
                //noinspection unchecked
                writeList((float[]) obj);
            } else if (obj instanceof double[]) {
                //noinspection unchecked
                writeList((double[]) obj);
            } else if (obj instanceof BigDecimal) {
                //noinspection unchecked
                writeDecimal((BigDecimal) obj);
            } else {
                throw new RuntimeException(String.format("Unsupported object type: %s", obj.getClass()));
            }
            return this;
        }

        private void writeSize(int size, int h) throws IOException {
            if (size <= 0x08) {
                stream.write(h | size);
            } else {
                size -= 0x09;
                int n;
                for (n = 1; n < 4; n++) {
                    if (size < (1l << (n * 8)))
                        break;
                }
                stream.write(h | 0x08 | n);
                for (int i = n - 1; i >= 0; i--)
                    stream.write((size >> (i * 8)) & 0xFF);
            }
        }

        private void writeInteger(Number num, int h) throws IOException {
            BigInteger bi = num instanceof BigInteger ? (BigInteger) num : BigInteger.valueOf(num.longValue());
            byte[] b = bi.toByteArray();
            writeSize(b.length - 1, h);
            stream.write(b);
        }

        private void writeFloat(Number num) throws IOException {
            float fnum = num.floatValue();
            double dnum = num.doubleValue();
            if ((fnum == dnum) || Double.isNaN(dnum) || Double.isInfinite(dnum)) {
                int inum = Float.floatToIntBits(fnum);
                stream.write(BITS_FLOAT | 0x03);
                for (int i = 3; i >= 0; i--)
                    stream.write((inum >> (i * 8)) & 0xFF);
            } else {
                long inum = Double.doubleToLongBits(dnum);
                stream.write(BITS_FLOAT | 0x07);
                for (int i = 7; i >= 0; i--)
                    stream.write((int) (inum >> (i * 8)) & 0xFF);
            }
        }

        private void writeBytes(byte[] b) throws IOException {
            writeSize(b.length, BITS_BYTES);
            stream.write(b);
        }

        private void writeString(String s) throws IOException {
            byte[] b = s.getBytes("UTF-8");
            writeSize(b.length, BITS_STRING);
            stream.write(b);
        }

        private void writeList(double[] c) throws IOException {
            writeSize(c.length, BITS_LIST);
            for (double obj : c)
                write(obj);
        }

        private void writeList(Collection<Object> c) throws IOException {
            writeSize(c.size(), BITS_LIST);
            for (Object obj : c)
                write(obj);
        }

        private void writeList(Object[] c) throws IOException {
            writeSize(c.length, BITS_LIST);
            for (Object obj : c)
                write(obj);
        }

        private void writeList(int[] c) throws IOException {
            writeSize(c.length, BITS_LIST);
            for (int obj : c)
                write(obj);
        }

        private void writeList(long[] c) throws IOException {
            writeSize(c.length, BITS_LIST);
            for (long obj : c)
                write(obj);
        }

        private void writeList(float[] c) throws IOException {
            writeSize(c.length, BITS_LIST);
            for (float obj : c)
                write(obj);
        }

        private void writeMap(Map<String, Object> m) throws IOException {
            writeSize(m.size(), BITS_MAP);
            for (Map.Entry<String, Object> e : m.entrySet()) {
                write(e.getKey());
                write(e.getValue());
            }
        }

        private void writeDecimal(BigDecimal d) throws IOException {
            writeInteger(d.unscaledValue(), BITS_DECIMAL);
            int e = -d.scale();
            if (e >= -0x40 && e < 0x40) {    // Single-byte scale
                stream.write(e & 0x7f);
            } else {                        // Multi-byte scale
                if (e >= 0)
                    e -= 0x40;
                else
                    e += 0x40;
                byte[] sb = BigInteger.valueOf(e).toByteArray();
                stream.write(0x80 | (sb.length - 1));
                stream.write(sb);
            }
        }
    }

    public static Object decode(byte[] bytes) throws IOException {
        return new MiceDecoder(new ByteArrayInputStream(bytes)).read();
    }

    public static byte[] encode(Object obj) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        new MiceEncoder(os).write(obj);
        return os.toByteArray();
    }
}
